﻿/// <summary>Initializes the AJAX Control Framework on the client-side.</summary>
(function() {    

    var createNamespace = function(fullTypeName) {
        var types = fullTypeName.split(".");
        var ns = "";
        var currentObj = window;

        for (var i = 0; i < types.length; i++)
        {
            var type = types[i];
            if (typeof currentObj[type] !== 'object' || currentObj[type] === null) { currentObj[type] = {}; }
            currentObj = currentObj[type];
        }
    };
    
    
    window["AjaxControlFramework"] = {};
    
    
    /// <summary></summary>
    /// <remarks></remarks>
    AjaxControlFramework.AjaxControl = function(uniqueID, autoSaveControlState) {
        if (!uniqueID) { throw new Error("The \"uniqueID\" parameter is required for the AjaxControlFramework.AjaxControl constructor."); }
        this.getUniqueID = function() { return uniqueID; };
        
        var id = function(id) {
            var delimiterIndex = id.lastIndexOf("$");
            if (delimiterIndex > 0)
            {
                return id.substring(delimiterIndex + 1);
            }
            else
            {
                return id;
            }
        }(uniqueID);
        
        this.getID = function() { return id; };

        var state = {};
        this.getState = function() { return state; };
        this.setState = function(value) { state = value; };
        
        if (!autoSaveControlState) { autoSaveControlState = true; }
        this.getAutoSaveControlState = function() { return autoSaveControlState; };
        this.setAutoSaveControlState = function(value) { autoSaveControlState = value; };
    };
    
    
    /* 
    Optional Parameters:
        returnHtml (boolean),
        autoUpdateHtml (boolean)
        onSuccess (function),
        onFailure (function),
        params (array),
        handleControlState (boolean),
        async (boolean) - 'true' by default
    */


    AjaxControlFramework.AjaxControl.prototype = {
        getUniqueID: this.getUniqueID,

        getState: this.getState,
        setState: this.setState,
        
        getAutoSaveControlState: this.getAutoSaveControlState,
        setAutoSaveControlState: this.setAutoSaveControlState,
        
        register: function() { AjaxControlFramework.AjaxControl.registeredControls[this.getUniqueID()] = this; },
        unregister: function() { delete AjaxControlFramework.AjaxControl.registeredControls[this.getUniqueID()]; },


        /*
        Required Parameters: *none*
        */
        update: function(options) {
            AjaxControlFramework.AjaxControl.invokeCallbackMethod.call(this, "Update", {}, options);
        },


        /*
        Required Parameters: controlType (string)
        */
        globalUpdate: function(controlType, options) {
            AjaxControlFramework.AjaxControl.invokeCallbackMethod.call(this, "GlobalUpdate", { controlType: controlType }, options);
        },


        /* 
        Required Parameters: methodName (string), methodName (string)
        */
        invoke: function(methodName, options) {
            AjaxControlFramework.AjaxControl.invokeCallbackMethod.call(this, "Invoke", { methodName: methodName }, options);
        },


        /* 
        Required Parameters: controlType (string), methodName (string)
        */
        globalInvoke: function(controlType, methodName, options) {
            AjaxControlFramework.AjaxControl.invokeCallbackMethod.call(this, "GlobalInvoke", { controlType: controlType, methodName: methodName }, options);
        },
        
        
        
        findStartMarker: function() {
            // Check for cached start and end marker nodes.
            if (!this.startMarker || this.startMarker.parentNode == null || this.startMarker.parentNode.nodeType != 1)
            {
                // Search the DOM for the start marker, starting at the body of the document.
                var startMarker = AjaxControlFramework.AjaxControl.recursiveFindMarker(document.body, this.getUniqueID() + "_start");
                if (startMarker != null) { this.startMarker = startMarker; }
            }
        },
        
        
        findEndMarker: function() { 
            if ((this.startMarker && !this.endMarker) || (this.endMarker && this.endMarker.parentNode == null) || (this.endMarker && this.endMarker.parentNode.nodeType != 1))
            {
                // Search the DOM, starting after the start marker.
                var endMarker = AjaxControlFramework.AjaxControl.recursiveFindMarker(this.startMarker, this.getUniqueID() + "_end");
                if (endMarker != null) { this.endMarker = endMarker; }
            }
        },
        
        
        /// <summary>Replaces the AJAX Control's existing HTML with new HTML.</summary>
        /// <remarks>This function searched the DOM for the HTML comment nodes that mark the start and end of the 
        /// AJAX Control's markup, clears out the existing nodes in between the markers, and replaces it with 
        /// the new HTML provided.</remarks>
        /// <param name="html" optional="false" mayByNull="false" type="String">The new HTML to replace the existing 
        /// HTML of the AJAX Control.</param>
        updateHtml: function(html) {
            if (!html || html == null) { return; }
            
            this.findStartMarker();
            this.findEndMarker();
            
            if (this.startMarker && this.endMarker)
            {
                // Remove all nodes between the start and end markers.
                var currentNode = this.startMarker.nextSibling, nextNode = null;
                
                while (currentNode != null && (nextNode = currentNode.nextSibling) && nextNode != this.endMarker)
                {
                    currentNode.parentNode.removeChild(currentNode);
                    currentNode = nextNode;
                }
                
                if (this.endMarker.previousSibling != null)
                {
                    this.endMarker.parentNode.removeChild(this.endMarker.previousSibling);
                }
                
                // Insert the specified new HTML as a new node before the end marker node.
                var emptyElement = document.createElement("ins");
                emptyElement.innerHTML = html;
                
                
                function recursiveCloneNode(node)
                {
                    var clonedNode = node.cloneNode(false);
                    
                    if (node.childNodes)
                    {
                        for (var index = 0; index < node.childNodes.length; index++)
                        {
                            clonedNode.appendChild(recursiveCloneNode(node.childNodes[index]));
                        }
                    }
                    
                    if (clonedNode.nodeName == "SELECT")
                    {
                        clonedNode.selectedIndex = node.selectedIndex;
                    }
                    
                    return clonedNode;
                }
                
                
                var placeholder = document.createDocumentFragment();
                for (var index = 0; index < emptyElement.childNodes.length; index++)
                {
                    placeholder.appendChild(recursiveCloneNode(emptyElement.childNodes[index]));
                }
                
                this.endMarker.parentNode.insertBefore(placeholder, this.endMarker);
                
                if (this.loadFunction) { this.loadFunction(); }
            }
        },
        
        
        onLoad: function(loadFunction) {
            var uniqueID = this.getUniqueID();
            
            this.loadFunction = function() 
            {
                if(typeof window[loadFunction] === 'function') 
                {
                    window[loadFunction](AjaxControlFramework.AjaxControl.registeredControls[uniqueID]);
                }
            };
            
            AjaxControlFramework.Script.Utils.onLoad(this.loadFunction);
        },
        
        
        // This function is used instead of the standard WebForm_InitCallback for preparing the POST 
        // request URL of the callback. It has an advantage over WebForm_InitCallback in that it only 
        // collects postback data for elements associated with the specific AJAX Control invoking 
        // the callback, instead of gathering the values of ALL of the postback elements on the entire 
        // Page. This results in less bandwidth usage and shorter execution times. Also, AND MOST 
        // IMPORTANT, it does NOT include any postback data involving ViewState.
        initCallback: function() {
            var callbackData = [];
            
            // Gather the postback data for the __EVENTTARGET and EVENTARGUMENT form inputs.
            callbackData.push("__EVENTTARGET=" + encodeURIComponent(document.forms[0].__EVENTTARGET ? document.forms[0].__EVENTTARGET.value : "") + "&");
            callbackData.push("__EVENTARGUMENT=" + encodeURIComponent(document.forms[0].__EVENTARGUMENT ? document.forms[0].__EVENTARGUMENT.value : "") + "&");
            
            
            // Find and cache the <!--ID_start--> and <!--ID_end--> markers surrounding the AJAX Control.
            this.findStartMarker();
            this.findEndMarker();
            
            var currentNode = null, nextNode = null;
            
            if (!this.startMarker)
            {
                this.startMarker = document.body.firstChild;
                currentNode = this.startMarker;
            }
            else
            {
                currentNode = this.startMarker.nextSibling;
            }
            
            if (!this.endMarker)
            {
                this.endMarker = document.body.lastChild;
            }
            
            
            function recursiveFormElementSearch(element)
            {
                if (element.tagName === "INPUT")
                {
                    var type = element.type;
                    var id = element.id;
                    
                    if ((type === "text" || type === "hidden" || type === "password" || ((type === "checkbox" || type === "radio") && element.checked)) 
                            && id !== "__EVENTVALIDATION" && id !== "__VIEWSTATE" && id !== "__VIEWSTATEENCRYPTED") // Don't include viewstate! BLARRRGGGG!!!
                    {
                        callbackData.push(element.name + "=" + encodeURIComponent(element.value) + "&");
                    }
                }
                else if (element.tagName === "SELECT" && element.selectedIndex !== -1)
                {
                    for (var index = 0; index < element.options.length; index++)
                    {
                        if (element.options[index].selected)
                        {
                            callbackData.push(element.name + "=" + encodeURIComponent(element.options[index].value) + "&");
                        }
                    }
                }
                else if (element.tagName === "TEXTAREA")
                {
                    callbackData.push(element.name + "=" + encodeURIComponent(element.value) + "&");
                }
                
                
                if (element.children && element.tagName !== "SELECT") // Check for SELECT so OPTIONS aren't iterated. Would be a waste.
                {
                    for (var index = 0; index < element.children.length; index++)
                    {
                        recursiveFormElementSearch(element.children[index]);
                    }
                }
            }
            
            
            if (this.startMarker && this.endMarker)
            {
                // Iterate all nodes between the start and end markers.                
                while (currentNode !== null && currentNode !== this.endMarker && (nextNode = currentNode.nextSibling) && (currentNode.nodeType === 1 || currentNode.nodeType === 3)) // IE doesn't pick up on text nodes. Webkit and Gecko do though.
                {
                    if (currentNode.nodeType === 1)
                    {
                        recursiveFormElementSearch(currentNode);
                    }
                    
                    currentNode = nextNode;
                }
            }
            
            return callbackData.join("");
        },
        
        
        invokeCallback: function(callbackID, callbackParam, callbackData, context, onSuccess, onFailure, async) {
            var ajaxRequest = null;
            if (!async) { async = true; }
            
            if (window.XMLHttpRequest)
            {
                ajaxRequest = new XMLHttpRequest();
            }
            else if (window.ActiveXObject)
            {
                ajaxRequest = new ActiveXObject("Microsoft.XMLHTTP");
            }
            else
            {
                // AJAX is not supported in the current browser. Fire the onFailure handler 
                // immediately instead of throwing an Error, keeping to the theme of transparency.
                onFailure({
                    Name: "AJAX not supported.", 
                    Message: "The current browser does not provide an implementation for the XMLHttpRequest object."
                }, context);
            }
                        
            
            ajaxRequest.onreadystatechange = function() { 
                if (ajaxRequest.readyState == 4)
                {                    
                    WebForm_ExecuteCallback({ 
                        xmlRequest: ajaxRequest,
                        eventCallback: (typeof onSuccess == "function" ? onSuccess : function() { return false; }),
                        errorCallback: (typeof onFailure == "function" ? onFailure : function() { return false; }),
                        context: context
                    });
                }
            };
            
            ajaxRequest.open("POST", document.forms[0].action, async);
            
            if (ajaxRequest.setRequestHeader)
            {
                ajaxRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            }
            
            callbackData += "__CALLBACKID=" + encodeURIComponent(callbackID) + "&__CALLBACKPARAM=" + encodeURIComponent(callbackParam);
            ajaxRequest.send(callbackData);
        }
    };
    
    
    AjaxControlFramework.Script = {};
    AjaxControlFramework.Script.Utils = {};
    AjaxControlFramework.Script.Utils.createNamespace = createNamespace;


    var handlerId = 1;
    
    AjaxControlFramework.Script.Utils.onLoad = function(handler) {
        if (!handler.id) { handler.id = handlerId++; }
        if (!AjaxControlFramework.Script.Utils.loadHandlers) { AjaxControlFramework.Script.Utils.loadHandlers = {}; }
        
       AjaxControlFramework.Script.Utils.loadHandlers[handler.id] = handler;
    };
    
    
    AjaxControlFramework.Script.Utils.displayException = function(exception) {
        alert(exception.Name + "\n" + exception.Message);
    };
    
    
    // Execute the onLoad handlers once the DOM has finished loading.
    // Insanely mad props go out to Matthias Miller, Dean Edwards, and John Resig
    // whose efforts and juicy brains engineered the following code that detects 
    // when the DOM has finished loading in the 4 major rendering engines.
    
    
    var loadTimer;
    AjaxControlFramework.Script.isLoaded = false;
    
    var fireOnLoad = function()
    {
        if (AjaxControlFramework.Script.isLoaded) { return; }
        
        if (AjaxControlFramework.Script.Utils.loadHandlers)
        {
            for (var id in AjaxControlFramework.Script.Utils.loadHandlers)
            {
                AjaxControlFramework.Script.Utils.loadHandlers[id]();
            }
         
            AjaxControlFramework.Script.isLoaded = true;
        }
    };
    
    
    if (document.addEventListener)
    {
        // Gecko, Presto, and WebKit implementation.
        document.addEventListener("DOMContentLoaded", function() {
            document.removeEventListener("DOMContentLoaded", arguments.callee, false);
            fireOnLoad();
        }, false);
    }
    else if (document.attachEvent)
    {
        // Trident implementation - Thanks to Matthias Miller (http://blog.outofhanwell.com/2006/06/08/the-windowonload-problem-revisited/).
        document.write("<script id='__ajax_id_onload' defer src='javascript:void(0)'><\/script>");
        
        var script = document.getElementById("__ajax_id_onload");
        script.onreadystatechange = function() {
            
            if (this.readyState == "complete") {
                fireOnLoad();
            }
        };
    }
    
    
    // Just in case, attach the load handler to the window.onload event.
    if (typeof window.onload == "function")
    {
        var onloadFunc = window.onload;
        
        window.onload = function() {
            onloadFunc();
            fireOnLoad();
        };
    }
    else
    {
        window.onload = fireOnLoad;
    }
    
    


    AjaxControlFramework.Script.JavascriptSerializer = {};
    
    
    
    /// <summary></summary>
    /// <remarks></remarks>
    AjaxControlFramework.Script.JavascriptSerializer.serialize = function(obj) {        
        if (!obj || obj.DoNotSerialize) { return; } // Opt-out.
        
        var json = [];

        if (obj.serialize) { return obj.serialize(); }
        else
        {
            if (typeof obj !== "object") { return "null"; }
            else
            {            
                for (var property in obj)
                {
                    if (!obj[property]) { continue; }
                    
                    if (obj[property] === null)
                    {
                        json.push('"' + property + '":null');
                        continue;
                    }
                    
                    if (!obj.propertyIsEnumerable(property) || obj[property].DoNotSerialize) { continue; }
                                
                    var propertyType = typeof obj[property];
                    
                    if (typeof obj[property] === "function") { continue; }
                    
                    json.push('"' + property + '":' + AjaxControlFramework.Script.JavascriptSerializer.serialize(obj[property]));
                }
            }
        }

        return "{" + json.join(",") + "}";
    };

    
    
    /// <summary></summary>
    /// <remarks></remarks>
    AjaxControlFramework.Script.JavascriptSerializer.deserialize = function(str) {
        if (typeof str != "string")
        {
            throw new TypeError("Only JSON strings can be deserialized.");
        }
        
        return eval('(' + (str === null || str.length === 0 ? 'null' : str) + ')');
    };


    AjaxControlFramework.Script.JavascriptSerializer.escapeCharacters = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
    AjaxControlFramework.Script.JavascriptSerializer.escapeReplacements = { "\"": "\\\"", "\\": "\\\\", "\b": "\\b", "\f": "\\f", "\n": "\\n", "\r": "\\r", "\t": "\\t" };
    
    
    
    /// <summary></summary>
    /// <remarks></remarks>
    Array.prototype.serialize = function() {
        if (this.DoNotSerialize) { return; } // Opt-out.
        
        var json = [];
        
        for (var i = 0; i < this.length; i++)
        {
            if (!this[i] || this[i].DoNotSerialize) { continue; }
            
            if (this[i] === null)
            {
                json.push("null");
            }
            else if (this[i].serialize)
            {
                json.push(this[i].serialize());
            }
            else
            {
                json.push(AjaxControlFramework.Script.JavascriptSerializer.serialize(this[i]));
            }
        }
        
        return "[" + json.join(",") + "]";
    };

    
    
    /// <summary></summary>
    /// <remarks></remarks>
    Function.prototype.serialize = function() {
        throw new TypeError("Functions cannot be convert to JSON.");
    };
    
    
    
    /// <summary></summary>
    /// <remarks></remarks>
    String.prototype.serialize = function() {
        if (AjaxControlFramework.Script.JavascriptSerializer.escapeCharacters.test(this))
        {
            return "\"" + this.replace(AjaxControlFramework.Script.JavascriptSerializer.escapeCharacters, function(match) {
                var replacement = AjaxControlFramework.Script.JavascriptSerializer.escapeReplacements[match];

                if (typeof replacement == "string")
                {
                    return replacement;
                }
                else
                {
                    return "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
                }
            }) + "\"";
        }
        else
        {
            return "\"" + this + "\"";
        }
    };
    
    
    
    /// <summary></summary>
    /// <remarks></remarks>
    Date.prototype.serialize = function() {
        return '"\\/Date(' + this.getTime() + ')\\/"';
    };

    
    
    /// <summary></summary>
    /// <remarks></remarks>
    Number.prototype.serialize = function() {
        return this.toString();
    };

    
    
    /// <summary></summary>
    /// <remarks></remarks>
    Boolean.prototype.serialize = function() {
        return this.valueOf();
    };
    
    
    // Global Properties
    AjaxControlFramework.AjaxControl.registeredControls = {};
    
    AjaxControlFramework.AjaxControl.defaults = {
        displayExceptionWhenOnFailureIsUndefined: true
    };
    
    AjaxControlFramework.AjaxControl.Settings = AjaxControlFramework.AjaxControl.defaults;
    
    AjaxControlFramework.AjaxControl.restoreDefaults = function() {
        AjaxControlFramework.AjaxControl.Settings = AjaxControlFramework.AjaxControl.defaults;
    };

    
    
    /// <summary></summary>
    /// <remarks></remarks>
    AjaxControlFramework.AjaxControl.invokeCallbackMethod = function(callbackMethodName, required, optional) {
        var params = {};
        if (!optional) { optional = {}; }
        
        var async = true;

        // Append all of the required parameters to the "params" object.
        for (var p in required) { params[p] = required[p]; }

        // Append the valid optional parameters to the "params" object (if they exist).
        if (optional.returnHtml) { params.returnHtml = optional.returnHtml; }
        if (optional.autoUpdateHtml) { params.autoUpdateHtml = optional.autoUpdateHtml; }
        if (optional.onSuccess) { params.onSuccess = optional.onSuccess; }
        if (optional.onFailure) { params.onFailure = optional.onFailure; }
        if (optional.params) { params.parameters = optional.params; }
        if (optional.async) { async = optional.async; }
        
        if (typeof optional.handleControlState === "boolean" && optional.handleControlState != this.getAutoSaveControlState()) { params.handleControlState = optional.handleControlState; }
        else { params.handleControlState = this.getAutoSaveControlState(); }


        // These parameters are ALWAYS required by the AJAX Control callback strategy.
        params.callbackMethod = callbackMethodName;
        params.state = this.getState();


        // Initialize the ASP.NET Callback mechanism.
        __theFormPostData = "";
        __theFormPostCollection = [];
        
        var callbackData = this.initCallback();

        var self = this;

        // Invoke an ASP.NET Callback.
        this.invokeCallback(this.getUniqueID(), AjaxControlFramework.Script.JavascriptSerializer.serialize(params), callbackData, { CommandName: callbackMethodName }, function(results, context) {
            var resultsObj = AjaxControlFramework.Script.JavascriptSerializer.deserialize(results);
            self.setState(resultsObj.State);
            
            if (((context.CommandName == "Update" || context.CommandName == "GlobalUpdate") && params.autoUpdateHtml && resultsObj.Html) || ((context.CommandName == "Invoke" || context.CommandName == "GlobalInvoke") && params.autoUpdateHtml && resultsObj.Html))
            {
                self.updateHtml(resultsObj.Html);
            }
            
            if (params.onSuccess)
            {
                setTimeout(function() {
                    params.onSuccess(resultsObj, context.CommandName);
                }, 1);
            }
        }, function(exception, context) {
            var exceptionObj = AjaxControlFramework.Script.JavascriptSerializer.deserialize(exception.substring(0, exception.lastIndexOf("}e") + 1));
            
            if (params.onFailure) 
            {
                setTimeout(function() {
                    params.onFailure(exceptionObj, context.CommandName);
                }, 1);
            }
            else if (AjaxControlFramework.AjaxControl.Settings.displayExceptionWhenOnFailureIsUndefined === true)
            {
                AjaxControlFramework.Script.Utils.displayException(exceptionObj);
            }
        }, async);
    };
    
    
    
    /// <summary></summary>
    /// <remarks></remarks>
    AjaxControlFramework.AjaxControl.recursiveFindMarker = function(currentNode, markerContent) {
        if (!currentNode) { return undefined; }
        
        do
        {
            if (currentNode.nodeType == 8 && currentNode.nodeValue.indexOf(markerContent) > -1)
            {
                return currentNode;
            }
            else if (currentNode.nodeType == 1)
            {
                var node = AjaxControlFramework.AjaxControl.recursiveFindMarker(currentNode.firstChild, markerContent);
                if (node != null) { return node; }
            }
        }
        while(currentNode = currentNode.nextSibling);
        
        return undefined;
    };

})();